# -*- coding: utf-8 -*-
# standard
import os
import sys
import warnings
import traceback
import inspect
from functools import partial
from datetime import datetime
from datetime import date
from datetime import timedelta
from typing import Union
from typing import Callable
from typing import List
from typing import Dict
from typing import Tuple
from typing import Optional
from typing import Any
from decimal import Decimal
from urllib.parse import quote_plus
from time import sleep

# third
from cachelib import FileSystemCache
# 导入 pydantic
from pydantic import __version__
major = __version__.split(".")[0]
if major == "1":  # v1 版本
    from pydantic import validate_arguments
    from pydantic import BaseModel
    from pydantic import Field
    from pydantic import constr
else:  # v2 版本
    from pydantic.v1 import validate_arguments
    # from pydantic.v1.decorator import validate_arguments
    from pydantic.v1 import BaseModel
    from pydantic.v1 import Field
    from pydantic.v1 import constr
# 导入 sqlalchemy
import sqlalchemy
from sqlalchemy import exc
from sqlalchemy import schema
from sqlalchemy import create_engine
from sqlalchemy import or_
from sqlalchemy import and_
from sqlalchemy import text
from sqlalchemy import case
from sqlalchemy.orm import Query
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import load_only
from sqlalchemy.orm.session import Session as DbSession
from sqlalchemy import select
from sqlalchemy import update
from sqlalchemy import delete
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import SmallInteger
from sqlalchemy import String
from sqlalchemy import Text
from sqlalchemy import Numeric
from sqlalchemy import DateTime
from sqlalchemy import JSON
from sqlalchemy import func

# local
_P_PATH =  os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
if _P_PATH not in sys.path:
    sys.path.append(_P_PATH)
from toolbox.common_tools import R_P_PATH
from toolbox.common_tools import logger
from toolbox.common_tools import DEBUG
from toolbox.common_tools import paging_data



print(f"当前 sqlalchemy 版本号： {sqlalchemy.__version__}")
db_file = os.path.join(R_P_PATH, "db", "scp_by_folder.db")
if not os.path.exists(os.path.dirname(db_file)):
    os.makedirs(os.path.dirname(db_file))
db_url = f"sqlite:///{db_file}"
engine = create_engine(url=db_url,
                    pool_recycle=86400,  # 超时回收
                    # pool_use_lifo=False,  # 后进先出（默认是先进先出）
                    pool_size=30,  # 连接池，过小会导致等待空余连接超时
                    max_overflow=10,  # 连接池溢出数
                    echo=False,  # 如果为真，引擎将记录所有语句以及 repr() 其参数列表的默认日志处理程序，默认为 sys.stdout 用于输出。
                    echo_pool=False,  # 如果为true，则连接池将记录信息输出
                    # pool_reset_on_return='rollback',
                    # future=True,
                    )
Base = declarative_base()
SESSION_FACTORY = sessionmaker(bind=engine)
DB_SESSION: Session = SESSION_FACTORY()
# DB_SESSION.autocommit = False


def new_db_session() -> Session:
    """
    创建新session，多线程下使用,注意：
    * 如果是sqlite在进行写操作时，不建议使用次方法，因为sqlite不能多线程写，会 database locked， 或者你自行实现多线程锁
    * pg 暂时没发现次问题
    """
    # todo: 避免使用工厂，防止数据库锁定问题的发生
    # global DB_SESSION
    # if not DB_SESSION:
    #     DB_SESSION = SESSION_FACTORY()
    # return DB_SESSION
    db_session = SESSION_FACTORY()
    return db_session


# 生产环境用


JSON_ENCODERS = { # BaseModel json编码器
            datetime: lambda v: v.strftime("%Y-%m-%d %H:%M:%S.%f"),
            date: lambda v: v.strftime("%Y-%m-%d"),
        }


class Resp(BaseModel):
    """
    返回消息的载体
    用于统一返回体
    可以直接被fastapi返回为json格式，不过其本质上还是调用了Resp.json函数。
    """
    message: str = Field("success", title="消息")
    data: Any = Field(None, title="数据载荷")

    class Config:
        json_encoders = JSON_ENCODERS

    def __bool__(self):
        mes = self.message
        if isinstance(mes, str) and mes.lower() == "success":
            return True
        else:
            return False

    def error(self, e_or_message: Union[Exception, str], **kwargs) -> str:
        """
        包装错误/异常
        :param e_or_message: 一个异常类的实例或者一段提示消息
        :return:
        """
        if isinstance(e_or_message, (str, Exception)):
            if isinstance(e_or_message, Exception):
                e_type = e_or_message.__class__
                if isinstance(e_or_message.args, tuple) and len(e_or_message.args) > 0:
                    tip = f"{e_or_message.args[0]}"
                else:
                    tip = ''
                message = f"{e_type.__name__}:{tip}"               
                logger.exception(e_or_message)
            else:
                if DEBUG:
                    error_position = self.get_stack_info()
                    message = f"{e_or_message}; {error_position}" if error_position else e_or_message
                else:
                    message = e_or_message
                logger.error(message)
            self.message = message              
            return self.message
        else:
            ms = "参数 e_or_message 必须是一个异常类的实例或者字符串对象: e_or_message = {}".format(
                e_or_message)
            raise ValueError(ms)

    @staticmethod
    def get_stack_info() -> str:
        """
        获取异常的堆栈相关的信息, 返回第一个栈,打印所有异常栈
        :return: _description_
        :yield: _description_
        """
        # 获取调用 resp.error 函数的位置
        first_error_position = ''
        # 获取每一帧栈, 是个耗时的操作
        begin = datetime.now()
        """
        返回调用者的栈的帧记录列表。第一个记录代表调用者,但是耗费资源
        inspect.stack() 约 0.13-0.15秒
        inspect.stack(0) 不加载 源码和源文件上下文,约耗时 0.0015-0.0018秒,
        这会节省一些时间,但会导致 code_context, index 为None
        """  
        func_names = ['error', 'get_stack_info']      
        stacks = inspect.stack(0) 
        end = datetime.now()
        logger.info(f"计算帧位置耗时: {(end - begin).total_seconds()}")
        length = len(stacks)
        if length < 1:
            # 一般不可能出现这种情况
            logger.error(f"帧越界: {stacks}")                   
        else:
            flag = False
            # 找到第一个不属于文件的帧
            current_file = __file__
            for frame in stacks:
                if flag or (frame.filename != current_file and frame.function not in func_names):
                    # frame_info 是一个 具名元组 FrameInfo(frame, filename, lineno, function, code_context, index) 的列表
                    error_position = f"代码位置: {frame.filename}第{frame.lineno}行的{frame.function}函数"
                    logger.error(error_position)
                    if not flag:
                        flag = True
                        first_error_position = error_position
        return first_error_position
    

@validate_arguments(config={"arbitrary_types_allowed": True})
def page(query: Query, page_num: int = 1, page_size: int = 10, 
         only: List[str] = None, ignore: List[str] = None, **kwargs) -> Resp:
    """
    为sqlalchemy 准备的分页查询函数， 注意这个 resp.data 是个Query 对象
    :param query: _description_
    :param page_num: 页码, defaults to 1
    :param page_size: 每页数量, defaults to 10
    :param only:允许的字段， 比 ignore 优先
    :param ignore: 忽略的字段
    :raises ValueError: _description_
    :return: _description_
    """
    total = query.count()
    offset = (page_num - 1) * page_size
    limit = page_num * page_size
    query = query.offset(offset).limit(limit)
    data = [x.to_dict(only=only, ignore=ignore) for x in query]
    data = paging_data(data=data, total=total, page_num=page_num, page_size=page_size)
    resp = Resp(data=data)
    return resp


class OrmTools:
    # sqlalchemy 的类型辅助工具

    @classmethod
    def get_columns(cls, only_name: bool = False) -> List[Union[Column, str]]:
        """
        返回模型定义中列的数组
        :return: _description_
        """
        data = cls.__mapper__.columns._all_columns
        if only_name:
            data = [x.name for x in data]
        return data

    def to_dict(self, only: List[str] = None, ignore: List[str] = None) -> dict:
        """
        实例转字典
        :param only: _description_, 优先 only
        :param ignore: _description_, defaults to None
        :return: _description_
        """
        columns = self.__mapper__.columns._all_columns
        data = {}
        if not only or len(only) == 0:
            only = []
            if not ignore or len(ignore) == 0:
                ignore = []
        else:
            if not ignore or len(ignore) == 0:
                ignore = []
        only_empty = len(only) == 0
        ignore_empty = len(ignore) == 0
        for column in columns:
            name = column.name
            flag = False
            if only_empty:
                if ignore_empty:
                    flag = True
                else:
                    flag = False if name in ignore else True
            else:
                flag = True if name in only else False
            if flag:
                data[name] = getattr(self, name)
        return data



class MyBaseModel(BaseModel):
    """
    带ORM功能的 pydantic.BaseModel
    """

    class Config:
        anystr_strip_whitespace = True  # 去字符串前后空格
        arbitrary_types_allowed = True  # 允许任意类型
        allow_population_by_field_name = True  # 允许使用模型属性给出的名称填充别名字段
        json_encoders = JSON_ENCODERS



class FileCacheManager:
  """
  文件系统缓存
  无论你调用 FileCacheManager的类方法还是 __FILE_CACHE__ 的实例方法效果都是一样的
  timeout = None 表示采用默认值
  timeout
  """
  cache: FileSystemCache

  def __init__(self) -> None:
    cache_dir = os.path.join(R_P_PATH, "cache")
    if not os.path.exists(cache_dir):
      os.makedirs(cache_dir)
    self.cache = FileSystemCache(cache_dir=cache_dir, threshold=50)

  @staticmethod
  def set(key: str, value: dict, timeout=300):
    return __FILE_CACHE__.cache.set(key, value, timeout)

  @staticmethod
  def set_many(mapping: Dict[str, Any], timeout=300):
    return __FILE_CACHE__.cache.set_many(mapping=mapping, timeout=timeout)
  
  @staticmethod
  def delete(key: str):
     return __FILE_CACHE__.cache.delete(key)
  
  @staticmethod
  def delete_many(*keys: str):
     return __FILE_CACHE__.cache.delete_many(*keys)
  
  @staticmethod
  def get(key: str):
     return __FILE_CACHE__.cache.get(key)
  
  @staticmethod
  def get_many(*keys: str):
     return __FILE_CACHE__.cache.get_many(*keys)
  

__FILE_CACHE__ = FileCacheManager()



if __name__ == '__main__':
    # Base.metadata.tables['code'].drop(engine)

    Base.metadata.create_all(engine)

if __name__ == '__main__':
    pass